package com.culture.config.component;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.CookieSpecs;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.LaxRedirectStrategy;

import org.springframework.http.*;
import org.springframework.http.client.*;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.stereotype.Component;
import org.springframework.util.StreamUtils;
import org.springframework.web.client.ResourceAccessException;
import org.springframework.web.client.ResponseErrorHandler;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;

import javax.annotation.PostConstruct;

import com.culture.config.common.util.ExceptionUtil;
import com.culture.config.common.util.JSONUtil;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.UUID;

@Data
@Slf4j
@Component
public class RestTemplateEx extends RestTemplate implements ClientHttpRequestInterceptor, ResponseErrorHandler {
    private String clientName = "configuration";
    private String userAgent = "config/v1.0";    
    private String apiKey = "testApiKey";    

    /**
     *ClientHttpRequestFactory
     * @return  HttpComponentsClientHttpRequestFactory
     */
    private ClientHttpRequestFactory getClientHttpRequestFactory(){
        final HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
        final RequestConfig defaultConfig = RequestConfig.custom().setCookieSpec(CookieSpecs.STANDARD).build();
        final HttpClient httpClient = HttpClientBuilder.create()
                .setRedirectStrategy(new LaxRedirectStrategy())
                .setDefaultRequestConfig(defaultConfig)
                .build();
        factory.setHttpClient(httpClient);
        return factory;
    }

    @PostConstruct
    public void init(){
        List<HttpMessageConverter<?>> messageConverters = this.getMessageConverters();
        StringHttpMessageConverter stringConverter = new StringHttpMessageConverter();
        stringConverter.setWriteAcceptCharset(false);
        messageConverters.add(stringConverter);
        this.setMessageConverters(messageConverters);
        this.setInterceptors(Arrays.asList(this));
        this.setErrorHandler(this);
        this.setRequestFactory(this.getClientHttpRequestFactory());
    }

    public HttpHeaders getHttpHeaders(HttpHeaders headers){
        if(Objects.isNull(headers)){
            headers = new HttpHeaders(); 
        }       
        return headers;
    }   

    public <T, R> R action(String method, String uri, T params, Class<R> clazz) {       
        return this.action(method,uri,getHttpHeaders(null),params,clazz);
    }

    public <T, R> R action(String method, String uri, HttpHeaders headers, T params, Class<R> clazz) {
        if(StringUtils.endsWithIgnoreCase("post", method)){
            return this.post(uri,headers,params,clazz);
        }
        return this.get(uri,headers,clazz);
    }

    public <R> R get(String uri, HttpHeaders headers, Class<R> clazz) {
        try {
            ResponseEntity<R> responseEntity = this.exchange(uri,HttpMethod.GET, getHttpEntity(getHttpHeaders(headers),null), clazz);
            return getResponseBody(responseEntity);
        } catch (RestClientException e) {
            this.handleRestClientException(e);
        }
        return null;
    }

    public <T, R> R post(String uri, HttpHeaders headers, T params, Class<R> clazz) {
        try {
            ResponseEntity<R> responseEntity = this.postForEntity(uri, getHttpEntity(getHttpHeaders(headers),params), clazz);
            return getResponseBody(responseEntity);
        } catch (RestClientException e) {
            this.handleRestClientException(e);
        }
        return null;
    }
   
    private <T> HttpEntity<String> getHttpEntity(HttpHeaders headers, T params){
        if (Objects.isNull(headers)) {
            headers = new HttpHeaders();
        }
        if (Objects.isNull(headers.getContentType())) {
            headers.setContentType(MediaType.APPLICATION_JSON);
        }
        String requestParams = Objects.isNull(params) ? null : toJSONString(params);
        return new HttpEntity<>(requestParams, headers);
    }

    private <R> R getResponseBody(ResponseEntity<R> responseEntity){
        ExceptionUtil.businessExceptionWhenNull(responseEntity, "remote responseEntity error.");     
        int statusCodeValue = responseEntity.getStatusCodeValue();
        R body = responseEntity.getBody();
        if (HttpStatus.OK.value() != statusCodeValue) {
            log.error("[{}], status code: {},body:{}",clientName, statusCodeValue, body);
        }
        return body;
    }

    private void handleRestClientException(RestClientException e){
        log.error(e.getMessage(), e);
        if (e instanceof ResourceAccessException) {
            throw e;
        }
        ExceptionUtil.businessException("remote request error");
    }

    private <T> String toJSONString(T params) {
        return JSONUtil.beanToJSONString(params);
    }

    private ClientHttpResponse trackResponse(ClientHttpResponse response,String trackId) throws IOException{
        final ClientHttpResponse responseCopy = new ClientHttpResponseWrapper(response);
        log.info("\n========={} response begin========\ntrackId: {}\nStatus code: {}\nRes body: {}\n=========={} response end=========",
                clientName, trackId, response.getStatusCode(), IOUtils.toString(responseCopy.getBody(), StandardCharsets.UTF_8.name()), clientName);
        return responseCopy;
    }

    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
        HttpHeaders headers = request.getHeaders();
        headers.set(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);
        headers.set(HttpHeaders.ACCEPT_CHARSET, StandardCharsets.UTF_8.name());
        if (Objects.isNull(headers.getContentType())) {
            headers.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
        }
        headers.set(HttpHeaders.USER_AGENT, userAgent);
        headers.set(HttpHeaders.AUTHORIZATION, this.getApiKey());
        String trackId = UUID.randomUUID().toString();
        log.info("\n======= {} request begin ======== \ntrackId: {}\nuri: {}\nmethod: {}\nheaders:{}\nrequest body: {}\n======= {} request end ========",
                clientName, trackId, request.getURI(), request.getMethod(), request.getHeaders(), new String(body, StandardCharsets.UTF_8.name()), clientName);
        ClientHttpResponse response = execution.execute(request, body);
        HttpHeaders headersResponse = response.getHeaders();
        headersResponse.set(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);
        return trackResponse(response,trackId);
    }

    @Override
    public boolean hasError(ClientHttpResponse response) throws IOException {
        HttpStatus statusCode = response.getStatusCode();
        if (statusCode.is3xxRedirection()) {
            return false;
        }
        return statusCode.is2xxSuccessful();
    }

    @Override
    public void handleError(ClientHttpResponse response) throws IOException{
        final ClientHttpResponse responseCopy = new ClientHttpResponseWrapper(response);
        String responseBody = IOUtils.toString(responseCopy.getBody(), StandardCharsets.UTF_8.name());
        log.error("Request error statusCode:{}, responseBody:{}", response.getStatusCode() , responseBody);
        responseCopy.close();
    }

    /**
     * Buffering ClientHttpResponse
     */
    private class ClientHttpResponseWrapper implements  ClientHttpResponse{
        private final ClientHttpResponse response;
        private byte[] body;

        ClientHttpResponseWrapper(ClientHttpResponse response) {
            this.response = response;
        }

        @Override
        public HttpStatus getStatusCode() throws IOException {
            return this.response.getStatusCode();
        }

        @Override
        public int getRawStatusCode() throws IOException {
            return this.response.getRawStatusCode();
        }

        @Override
        public String getStatusText() throws IOException {
            return this.response.getStatusText();
        }

        @Override
        public HttpHeaders getHeaders() {
            return this.response.getHeaders();
        }

        @Override
        public InputStream getBody() throws IOException {
            if (this.body == null) {
                this.body = StreamUtils.copyToByteArray(this.response.getBody());
            }
            return new ByteArrayInputStream(this.body);
        }

        @Override
        public void close() {
            this.response.close();
        }
    }
}
